package com.hzw.saas.web.app.old.storage.controller;


import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Objects;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;

import com.hzw.saas.api.old.storage.*;
import com.hzw.saas.api.old.storage.bo.*;
import com.hzw.saas.api.old.storage.enums.ConflictModeEnum;
import com.hzw.saas.api.old.storage.enums.FileIntegrityEnum;
import com.hzw.saas.api.old.storage.enums.ResourceAuthEnum;
import com.hzw.saas.api.old.storage.enums.ResourceStatusEnum;
import com.hzw.saas.api.old.storage.exception.FileConflictException;
import com.hzw.saas.common.config.annotation.SysLog;
import com.hzw.saas.common.config.exception.DefaultSaasException;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.security.pojo.dto.SaasUser;
import com.hzw.saas.common.security.utils.SecurityUtils;
import com.hzw.saas.common.util.HttpContextUtils;
import com.hzw.saas.service.old.storage.annotation.FileAuthAfter;
import com.hzw.saas.service.old.storage.annotation.FileAuthBefore;
import com.hzw.saas.service.old.storage.annotation.FolderAuthBefore;
import com.hzw.saas.service.old.storage.util.StorageUtil;
import com.hzw.saas.service.storage.config.StorageConfig;
import com.hzw.saas.web.app.old.storage.dto.ResourceFilePostDto;
import com.hzw.saas.web.app.old.storage.dto.ResourceFolderPostDto;
import com.hzw.saas.web.app.old.storage.param.*;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ma.glasnost.orika.MapperFacade;


/**
 * <p>
 * 资源表 前端控制器
 * </p>
 *
 * @author jobob
 * @since 2020-12-23
 */
// @Controller
@RequiredArgsConstructor
// @Validated
// @Api(tags = "资源管理/个人云空间")
@Slf4j
public class UserResourceController {

    private final HttpServletRequest request;
    private final HttpServletResponse response;

    private final MapperFacade mapperFacade;
    private final StorageConfig storageConfig;
    private final IGroupResourceService groupResourceService;
    private final IResourceFileService fileService;
    private final IResourceFolderService folderService;
    private final IStorageService storageService;

    @Resource(name = "pInxChunkService")
    private InxChunkService chunkService;

    /************************ 统计接口 ************************/

    /************************ 目录操作接口 ************************/

    @GetMapping("/user/resource/space/p/folder/{folderId}/info")
    @SysLog
    @FolderAuthBefore(folderId = "#folderId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "获取目录结构", notes = "获取个人空间目录结构，可选获取子目录与子文件")
    @ApiImplicitParam(name = "folderId", value = "资源目录ID, 特殊：0-根目录", dataType = "String", required = true)
    @ApiOperationSort(21)
    public ResponseEntity<ResourceFolderBO> getFolderInfo(@PathVariable String folderId, @Validated ResourceFolderGetParam param) {
        SaasUser saasUser = SecurityUtils.getUser();
        param.init(folderId);
        ResourceFolderBO folderInfo;
        if (StorageUtil.isRootFolder(param.getFolderId(), false)) {
            folderInfo = folderService.getRootFolder(
                saasUser.getUserId(),
                storageConfig.getPersonalSpaceCatalog(),
                param.getPathLevel(),
                Objects.equals(param.getWithFile(), true),
                param.getStatus(),
                FileIntegrityEnum.NORMAL.code()
            );
        } else {
            folderInfo = folderService.getFolder(
                param.getFolderId(),
                param.getPathLevel(),
                Objects.equals(param.getWithFile(), true),
                param.getStatus(),
                FileIntegrityEnum.NORMAL.code(),
                true
            );
        }
        groupResourceService.assembleGroups(folderInfo);
        return ResponseEntity.ok(folderInfo);
    }

    @PostMapping("/user/resource/space/p/folders")
    @SysLog(operation = "创建资源目录", mode = 3)
    @FolderAuthBefore(folderId = "#param.folderId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "创建资源目录", notes = "创建目录，支持多级目录创建")
    @ApiOperationSort(22)
    public ResponseEntity<List<ResourceFolderPostDto>> createFolders(@Validated @RequestBody ResourceFolderPostParam param) {
        SaasUser user = SecurityUtils.getUser();
        List<ResourceFolderBO> folders = folderService.createFoldersByPath(
            user.getUserId(),
            user.getUserId(),
            storageConfig.getPersonalSpaceCatalog(),
            param.getFolderId(),
            param.getPath()
        );
        List<ResourceFolderPostDto> result = mapperFacade.mapAsList(folders, ResourceFolderPostDto.class);
        return ResponseEntity.ok(result);
    }

    @PutMapping("/user/resource/space/p/folder/info")
    @SysLog(operation = "更新目录信息", mode = 3)
    @FolderAuthBefore(folderId = "#param.folderId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "更新目录信息", notes = "更新目录信息，如父目录，目录名等")
    @ApiOperationSort(23)
    public ResponseEntity<Void> updateFolder(@Validated @RequestBody ResourceFolderPutParam param) {
        ResourceFolderBO folderInfo = mapperFacade.map(param, ResourceFolderBO.class);
        folderService.updateFolder(folderInfo, true);
        return ResponseEntity.ok().build();
    }

    @DeleteMapping("/user/resource/space/p/folder/{folderId}")
    @SysLog(operation = "删除目录", mode = 3)
    @FolderAuthBefore(folderId = "#folderId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "删除目录", notes = "彻底删除目录，同时会删除子目录和文件")
    @ApiOperationSort(24)
    public ResponseEntity<Void> deleteFolder(@PathVariable String folderId) {
        folderService.updateFolderStatus(folderId, ResourceStatusEnum.DELETED.code(), false);
        return ResponseEntity.ok().build();
    }

    /************************ 文件操作接口 ************************/

    @GetMapping("/user/resource/space/p/file/{fileId}/info")
    @SysLog
    @FileAuthAfter(auth = ResourceAuthEnum.READ)
    @ApiOperation(value = "获取文件信息", notes = "获取个人空间文件信息")
    @ApiImplicitParam(name = "fileId", value = "资源文件ID", dataType = "String", required = true)
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 401, message = "请求非法，该资源不属于本用户/资源不存在")
    })
    @ApiOperationSort(31)
    public ResponseEntity<ResourceFileBO> getFileInfo(@PathVariable String fileId) {
        ResourceFileBO resourceFileBo = groupResourceService.getGroupFile(fileId);
        return ResponseEntity.ok(resourceFileBo);
    }

    @PostMapping("/user/resource/space/p/file/info")
    @SysLog(operation = "提交文件信息", mode = 3)
    @FolderAuthBefore(folderId = "#param.folderId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "提交文件信息", notes = "提交资源文件信息，用于分片上传资源前获取服务器返回的资源唯一标识符(fileId)，若存在同名文件，当覆盖模式为false时，会返回409错误，当覆盖模式为true时，会覆盖同名文件")
    @ApiResponses({
        @ApiResponse(code = 200, message = "返回资源标识ID,用于后续上传操作"),
        @ApiResponse(code = 400, message = "请求参数错误"),
        @ApiResponse(code = 403, message = "无访问权限；单文件大小超过1G；存储总容量不足"),
        @ApiResponse(code = 409, message = "文件已存在"),
    })
    @ApiOperationSort(32)
    public ResponseEntity<ResourceFilePostDto> saveFileInfo(@Validated @RequestBody ResourceFilePostParam param) {
        log.debug("post resource info request...");
        // 操作用户
        SaasUser saasUser = SecurityUtils.getUser();
        // 判断文件大小是否超出
        AssertUtil.assertThrow("文件大小不可超过1G", HttpStatus.FORBIDDEN, this.fileOverSize(param.getLength()));
        // 判断用户存储空间是否足够
        AssertUtil.assertThrow("用户空间容量不足", HttpStatus.FORBIDDEN, this.userOverSize(saasUser.getUserId(), param.getLength()));
        // 保存信息
        ResourceFileBO fileInfo = mapperFacade.map(param, ResourceFileBO.class);
        fileInfo.setOwnerId(saasUser.getUserId());
        fileInfo.setCreatorId(saasUser.getUserId());
        fileInfo.setCatalog(storageConfig.getPersonalSpaceCatalog());
        fileInfo.setIntegrity(FileIntegrityEnum.UPLOADING.code());
        ResourceFileBO result = null;
        try {
            result = fileService.saveFileInfo(fileInfo, ConflictModeEnum.instance(Objects.equals(param.getCover(), true)));
        } catch (FileConflictException e) {
            throw DefaultSaasException.build("存在同名文件").status(HttpStatus.CONFLICT);
        }
        // 返回
        ResourceFilePostDto resourceFilePostDto = mapperFacade.map(result, ResourceFilePostDto.class);
        return ResponseEntity.ok(resourceFilePostDto);
    }

    /**
     * 单独校验每个分片
     *
     * @param chunkCheck
     * @return
     */
    @GetMapping("/user/resource/space/p/file/chunk")
    @SysLog
    @ApiOperation(value = "校验文件分片", notes = "用于分片插件开启分片上传校验时使用，如vue-simple-uploader中开启testChunks=true")
    @ApiResponses({
        @ApiResponse(code = 200, message = "分片已上传过"),
        @ApiResponse(code = 304, message = "分片未上传过"),
        @ApiResponse(code = 400, message = "请求参数异常"),
        @ApiResponse(code = 500, message = "服务器异常")
    })
    @ApiOperationSort(33)
    public ResponseEntity<Void> checkChunk(@Validated InxChunkBO chunkCheck) {
        log.debug("chunk check request...");
        // 设置分片校验状态信息
        // 默认返回非(200, 201, 202)状态码，正常走标准上传(不进行分片校验)
        int status = HttpServletResponse.SC_NOT_MODIFIED;

        // 检查resourceId是否有数据库记录
        /*Resource resource = resourceService.getById(chunkCheck.getResourceId());
        // 没有记录，抛出异常
        if (resource == null) {
            throw DefaultSaasException.build("无法通过文件ID获取记录，请检查操作或先使用/user/resource接口进行文件信息提交并获取文件唯一标识(ID)").status(HttpStatus.BAD_REQUEST);
        }*/

        // 通过文件ID检查数据库中文件是否存在且上传状态为已完成
        /*if (ObjectUtil.equal(ResourceStatus.UPLOADED.getCode(), resource.getStatus())) {
            status = HttpServletResponse.SC_OK;
        }*/

        // 比对文件摘要查看是否存在相同摘要的文件(实现秒传)
        /*if (StringUtils.isNoneBlank(chunkCheck.getDigest())) {
            Resource one = resourceService.getOne(Wrappers.<Resource>lambdaQuery().eq(Resource::getDigest, chunkCheck.getDigest()));
            if (one != null) {
                one.setResourceId(chunkCheck.getResourceId());
                one.setCreateUser(saasUser.getUserId());
                resourceService.updateById(resource);
                status = HttpServletResponse.SC_OK;
            }
        }*/

        // 检查上传状态
        boolean exist = chunkService.existChunk(storageConfig.getPersonalSpaceCacheCatalog(), chunkCheck);

        // 文件上传完成，可跳过上传（秒传）
        if (exist) {
            status = HttpServletResponse.SC_OK;
        }
        return ResponseEntity.status(status).build();
    }

    @PostMapping(value = "/user/resource/space/p/file/chunk", consumes = {"multipart/form-data"})
    @SysLog
    @ApiOperation(value = "上传文件分片", notes = "上传文件分片，该接口支持同一分片重复调用上传。上传文件前请先调用<提交个人空间文件信息>接口，获取服务器提供的文件ID")
    @ApiResponses({
        @ApiResponse(code = 200, message = "上传成功"),
        @ApiResponse(code = 400, message = "请求参数或请求类型异常"),
        @ApiResponse(code = 415, message = "文件类型不支持"),
        @ApiResponse(code = 500, message = "服务器内部错误")
    })
    @ApiOperationSort(35)
    public ResponseEntity<Boolean> uploadChunk(@Validated InxChunkDataBO param, HttpServletRequest request) {
        log.debug("chunk upload request...");
        // 判断请求类型
        boolean multipart = ServletUtil.isMultipart(request);
        if (!multipart) {
            throw DefaultSaasException.build("上传失败，请求参数异常，请使用multipart/form-data类型上传").status(HttpStatus.BAD_REQUEST);
        }
        try {
            // 此处直接将spring获取到的缓存文件放进内存
            // 这一步是为了防止后续在异步处理分片文件保存时，springboot将缓存文件删除
            // 分片保存
            byte[] data = param.getFile().getBytes();
            chunkService.saveChunk(storageConfig.getPersonalSpaceCacheCatalog(), param, data);
            return ResponseEntity.ok(true);
        } catch (IOException e) {
            // 上传失败，更新资源状态
            log.error("chunk upload error", e);
            throw DefaultSaasException.build(e.getMessage());
        }
    }

    @PutMapping("/user/resource/space/p/file/{fileId}/chunks")
    @SysLog(operation = "完成文件上传", mode = 3)
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "完成文件上传", notes = "文件分片上传完成后执行该回调方法")
    @ApiImplicitParam(name = "fileId", value = "资源文件ID", required = true, dataType = "String")
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 400, message = "文件未上传完整"),
        @ApiResponse(code = 500, message = "服务器内部错误"),
    })
    @ApiOperationSort(36)
    public ResponseEntity<Boolean> mergeChunks(@NotBlank(message = "资源ID不可为空") @PathVariable String fileId) {
        // 检查文件是否已上传完成
        ResourceFileBO fileInfo = fileService.getFileInfo(fileId, ResourceStatusEnum.NORMAL.code(), FileIntegrityEnum.NORMAL.code(), false);
        if (fileInfo != null) {
            log.info("文件({})已合并过,直接返回...", fileInfo);
            return ResponseEntity.ok(true);
        }
        // 合并分片
        fileInfo = fileService.getFileInfo(fileId, true);
        fileInfo.setName(fileInfo.getFileId());
        chunkService.mergeChunks(fileInfo, storageConfig.getPersonalSpaceCacheCatalog(), com.hzw.saas.common.util.enums.ConflictModeEnum.THROW_ERR);
        ResourceFileBO resourceFileBo = new ResourceFileBO()
            .setFileId(fileId)
            // .setResourceKey(key)
            .setIntegrity(FileIntegrityEnum.NORMAL.code());
        fileService.updateFileInfo(resourceFileBo, ConflictModeEnum.COVER_EXISTS, true);
        return ResponseEntity.ok(true);
    }

    @DeleteMapping("/user/resource/space/p/file/{fileId}/chunks")
    @SysLog(operation = "取消文件上传", mode = 3)
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "取消文件上传", notes = "取消文件上传，删除服务端已接收的文件分片和文件信息")
    @ApiImplicitParam(name = "fileId", value = "资源文件ID", required = true, dataType = "String")
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 500, message = "服务器内部错误"),
    })
    @ApiOperationSort(37)
    public ResponseEntity<Void> cancelUpload(@NotBlank(message = "资源ID不可为空") @PathVariable String fileId) {
        this.fileService.deleteFile(fileId);
        this.chunkService.deleteChunks(storageConfig.getPersonalSpaceCacheCatalog(), fileId);
        return ResponseEntity.ok().build();
    }

    @PutMapping("/user/resource/space/p/file/info")
    @SysLog(operation = "更新文件信息", mode = 3)
    @FileAuthBefore(fileId = "#param.fileId", auth = ResourceAuthEnum.OWN)
    @ApiOperation(value = "更新文件信息", notes = "更新文件信息，存在同名文件时会报409错误")
    @ApiResponses({
        @ApiResponse(code = 200, message = "返回资源标识ID,用于后续上传操作"),
        @ApiResponse(code = 409, message = "文件已存在"),
    })
    @ApiOperationSort(38)
    public ResponseEntity<Void> updateFileInfo(@Validated ResourceFilePutParam param) {
        ResourceFileBO fileBO = mapperFacade.map(param, ResourceFileBO.class);
        try {
            this.fileService.updateFileInfo(fileBO, ConflictModeEnum.THROW_EXISTS, true);
        } catch (Exception e) {
            throw DefaultSaasException.build("存在同名文件").status(HttpStatus.CONFLICT);
        }
        return ResponseEntity.ok().build();
    }

    @PutMapping("/user/resource/space/p/files/info")
    @SysLog(operation = "复制/移动目录/文件", mode = 3)
    @FileAuthBefore(fileIds = "#param.fileIds", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderId = "#param.targetFolderId", auth = ResourceAuthEnum.OWN)
    @Transactional(rollbackFor = Exception.class)
    @ApiOperation(value = "复制/移动目录/文件", notes = "复制/移动目录/文件，支持批量操作")
    @ApiResponses({
        @ApiResponse(code = 200, message = "操作成功"),
        @ApiResponse(code = 400, message = "请求参数错误"),
        @ApiResponse(code = 403, message = "无访问权限；存储总容量不足")
    })
    @ApiOperationSort(39)
    public ResponseEntity<Void> copyFile(@Validated ResourceFileCopyParam param) {
        SaasUser user = SecurityUtils.getUser();
        switch (param.getMode()) {
            // 复制
            case 1:
                boolean overSize = this.copyOverSize(user.getUserId(), param.getFileIds(), param.getFolderIds());
                AssertUtil.assertThrow("用户空间容量不足", HttpStatus.FORBIDDEN, overSize);
                this.fileService.copyFilesInfo(param.getFileIds(), param.getTargetFolderId(), ConflictModeEnum.RENAME_ORIGINAL);
                this.folderService.copyFoldersInfo(param.getFolderIds(), param.getTargetFolderId(), ConflictModeEnum.MERGE_EXISTS);
                break;
            // 移动
            case 2:
                this.fileService.moveFilesInfo(param.getFileIds(), param.getTargetFolderId(), ConflictModeEnum.RENAME_ORIGINAL);
                this.folderService.moveFoldersInfo(param.getFolderIds(), param.getTargetFolderId(), ConflictModeEnum.MERGE_EXISTS);
                break;
            default:
                throw DefaultSaasException.build("文件操作模式错误");
        }
        return ResponseEntity.ok().build();
    }

    @GetMapping("/user/resource/space/p/file/{fileId}")
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.READ)
    @SysLog(operation = "下载文件", mode = 3)
    @ApiOperation(value = "下载文件", notes = "下载个人空间文件")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "access_token", value = "用户token（或在请求头设置Authorization）", dataType = "String", paramType = "query"),
        @ApiImplicitParam(name = "fileId", value = "资源文件ID", required = true, dataType = "String"),
        @ApiImplicitParam(name = "fileName", value = "自定义文件名", dataType = "String")
    })
    @ApiResponses({
        @ApiResponse(code = 400, message = "请求异常"),
        @ApiResponse(code = 500, message = "服务器内部错误"),
    })
    @ApiOperationSort(40)
    public void downloadFile(@PathVariable String fileId, String fileName) {
        log.debug("download request...");
        ResourceFileBO resourceFileBo = fileService.getFileInfo(fileId, ResourceStatusEnum.NORMAL.code(), FileIntegrityEnum.NORMAL.code(), true);
        /*if (StringUtils.isBlank(resourceFileBo.getResourceKey())) {
            throw DefaultSaasException.build("无法获取文件存储key").status(HttpStatus.BAD_REQUEST);
        }*/
        try (OutputStream fos = response.getOutputStream()) {
            log.debug("start download file: " + resourceFileBo.getFileId());
            fileName = StrUtil.isBlank(fileName) ? resourceFileBo.getName() : fileName;
            HttpContextUtils.setFileDownloadHeader(request, response, fileName, resourceFileBo.getLength());
            fileService.downloadFile(storageConfig.getPersonalSpaceCatalog(), fileId, fos, 1);
            response.flushBuffer();
            log.debug("download file over...");
        } catch (IOException e) {
            log.error("download error", e);
            throw DefaultSaasException.build("下载文件异常");
        }
    }

    @DeleteMapping("/user/resource/space/p/file/{fileId}")
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "删除文件", mode = 3)
    @ApiOperation(value = "删除文件", notes = "删除个人空间文件")
    @ApiImplicitParam(name = "fileId", value = "资源文件ID", required = true, dataType = "String")
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求完成，true"),
        @ApiResponse(code = 400, message = "请求异常"),
        @ApiResponse(code = 401, message = "请求非法，该资源不属于本用户/资源不存在"),
        @ApiResponse(code = 500, message = "服务器内部错误"),
    })
    @ApiOperationSort(41)
    public ResponseEntity<Boolean> deleteFile(@NotBlank(message = "资源Id不可为空") @PathVariable String fileId) {
        log.debug("delete resource info request...");
        fileService.updateFilesStatus(fileId, ResourceStatusEnum.DELETED.code(), false);
        return ResponseEntity.ok(true);
    }

    private boolean fileOverSize(long length) {
        return length > storageConfig.getFileMaxSize();
    }

    private boolean userOverSize(String userId, long length) {
        StorageCapacityBO storageCapacity = storageService.getStorageCapacity(userId);
        long sizeUsed = Objects.isNull(storageCapacity.getUsedSize()) ? 0L : storageCapacity.getUsedSize();
        long totalSize = Objects.isNull(storageCapacity.getTotalSize()) ? 0L : storageCapacity.getTotalSize();
        return sizeUsed + length > totalSize;
    }

    private boolean copyOverSize(String userId, List<String> fileIds, List<String> folderIds) {
        StorageCapacityBO storageCapacity = storageService.getStorageCapacity(userId);
        long sizeUsed = Objects.isNull(storageCapacity.getUsedSize()) ? 0L : storageCapacity.getUsedSize();
        long totalSize = Objects.isNull(storageCapacity.getTotalSize()) ? 0L : storageCapacity.getTotalSize();
        long copySize = 0L;
        copySize += fileService.countFilesSize(fileIds);
        if (CollectionUtil.isNotEmpty(folderIds)) {
            copySize += folderIds.stream().mapToLong(folderService::countFolderSize).sum();
        }
        return sizeUsed + copySize > totalSize;
    }

}
